// Parse CommonJS require() statements.
//
// This is an LL(1) recursive descent parser which uses the tokenizer from
// Narcissus.  We avoid the parser since it's slow -- we just parse what we
// need.

var json = require('json'),
    printf = require('printf');

var log = require('oil/log');

var narcissus = require('narcissus');


var sprintf = printf.sprintf;


var parseDeps = exports.parseDeps = function(codeStr) {
  var tokenizer = new narcissus.Tokenizer(codeStr),
      p = new RequireParser(tokenizer);
  var result = p.parseUnit();
  return result;
}


var ParseError = function(message, lineno, start, end) {
  this.message = message;
  this.lineno = lineno;
  this.start = start;
  this.end = end;
}

ParseError.prototype = {
  toString: function() {
    return sprintf('ParseError: %s (line %d, columns %d-%d)',
        this.message,
        this.lineno,
        this.start,
        this.end);
  }
}


var RequireParser = function(tokenizer  ) {
  this.t = tokenizer;
  this.token = null;
  this.cursor = 0;

  this.nextToken();
}

RequireParser.prototype = {

  tryConsume: function(tokenType) {
    if (this.token.type === tokenType) {
      this.nextToken();
      return true;
    } else {
      return false;
    }
  },

  consume: function(tokenType) {
    var value = this.token.value;
    if (!this.tryConsume(tokenType)) {
      this.error('Expected ' + tokenType + ', got ' + this.token.type);
    }
    return value;
  },

  nextToken: function() {
    this.t.get();
    this.token = this.t.token();
    log.info(json.stringify(this.token));
  },

  error: function(message) {
    throw new ParseError(
        message, this.t.lineno, this.token.start, this.token.end);
  },

  // Parse a unit of code
  parseUnit: function() {
    var deps = [],
        offset = 0;
    while (true) {
      var result = this.parseRequireBlock();
      if (result) {
        deps.push.apply(deps, result.deps);
        offset = result.offset;
      } else {
        break;
      }
    }
    return {deps: deps, offset: offset};
  },

  // Returns a list of pairs, or null
  parseRequireBlock: function() {
    var deps = [],
        offset = 0;

    if (!this.tryConsume(narcissus.VAR)) {
      return null;
    }

    while (true) {
      var pair = this.parseInit();
      if (!pair) {
        return null;
      }
      deps.push(pair);
      if (this.token.type === narcissus.SEMICOLON) {
        offset = this.token.end;  // the semi-colon
        this.nextToken();
        break;  // done with this block
      } else if (this.token.type === narcissus.COMMA) {
        this.nextToken();
      } else {
        throw {name: 'SyntaxError',
               message: 'Expected , or ; -- got ' + this.token.type};
      }
    }
    return {deps: deps, offset: offset};
  },

  // Returns an module variable initialization pair, or null
  parseInit: function() {
    try {
      var varName = this.consume(narcissus.IDENTIFIER);
      this.consume(narcissus.ASSIGN);
      var funcName = this.consume(narcissus.IDENTIFIER);
      if (funcName !== 'require') {
        this.error('Expected "require"');
      }
      this.consume(narcissus.LEFT_PAREN);
      var modName = this.consume(narcissus.STRING);
      this.consume(narcissus.RIGHT_PAREN);
    } catch (e) {
      if (!(e instanceof ParseError)) {
        throw e;
      }
      return null;
    }
    return {modName: modName, varName: varName};
  }
}
